const FAN_HALF_FLUSH = 0
const FAN_FULL_FLUSH = 1
const FAN_ALL_HONORS = 2

const FAN_ALL_PUNGS = 3
const FAN_ALL_OUTSIDE = 4

const FAN_STRAIGHT = 5
const FAN_MIXED_TRIPLE = 6

const FAN_NAME = ['混一色', '清一色', '字一色', '碰碰和', '全带幺', '一条龙', '三相逢']

const BANRANK_ALL_PUNGS = (() => {
	let banRank = new Array(111).fill(false)
	for (let suit = 0; suit < 3; ++suit)for (let rank = 1; rank <= 7; ++rank) {
		banRank[80 + (rank << 2 | suit)] = true
	}
	return banRank
})()
const BANRANK_ALL_OUTSIDE = (() => {
	let banRank = new Array(111).fill(false)
	for (let suit = 0; suit < 3; ++suit) {
		for (let rank = 2; rank <= 8; ++rank) {
			banRank[rank << 2 | suit] = true
			banRank[40 + (rank << 2 | suit)] = true
		}
		for (let rank = 2; rank <= 6; ++rank) {
			banRank[80 + (rank << 2 | suit)] = true
		}
	}
	return banRank
})()

const REQS_STRAIGHT = (() => {
	let reqs = []
	for (let suit = 0; suit < 3; ++suit) {
		let req = []
		for (let rank = 1; rank <= 7; rank += 3) {
			req.push(80 + (rank << 2 | suit))
		}
		reqs.push(req)
	}
	return reqs
})()
const REQS_MIXED_TRIPLE = (() => {
	let reqs = []
	for (let rank = 1; rank <= 7; ++rank) {
		let req = []
		for (let suit = 0; suit < 3; ++suit) {
			req.push(80 + (rank << 2 | suit))
		}
		reqs.push(req)
	}
	return reqs
})()

function shantenLessThan(hand, s) {
	return _isHu(hand, s, 3)
		|| _isHu(hand, s, 5)
		|| _isHu(hand, s, 6)
		|| _isHu(hand, s, 0, BANRANK_ALL_PUNGS)
		|| _isHu(hand, s, 0, BANRANK_ALL_OUTSIDE)
		|| _isHu(hand, s, 0, 0, REQS_STRAIGHT[0])
		|| _isHu(hand, s, 0, 0, REQS_STRAIGHT[1])
		|| _isHu(hand, s, 0, 0, REQS_STRAIGHT[2])
		|| _isHu(hand, s, 0, 0, REQS_MIXED_TRIPLE[0])
		|| _isHu(hand, s, 0, 0, REQS_MIXED_TRIPLE[1])
		|| _isHu(hand, s, 0, 0, REQS_MIXED_TRIPLE[2])
		|| _isHu(hand, s, 0, 0, REQS_MIXED_TRIPLE[3])
		|| _isHu(hand, s, 0, 0, REQS_MIXED_TRIPLE[4])
		|| _isHu(hand, s, 0, 0, REQS_MIXED_TRIPLE[5])
		|| _isHu(hand, s, 0, 0, REQS_MIXED_TRIPLE[6])
}
function shanten(hand) {
	for (let joker = 0; ; ++joker) {
		if (joker > 13 || shantenLessThan(hand, joker + 1)) {
			return joker
		}
	}
}
function calc(comb) {
	let fan = []
	let suits = 0, allPungs = true, allOutside = true
	for (const set of comb) {
		const type = set[0], suit = (set[1] & 3)
		suits |= (1 << suit)
		if (type === CHOICE_CONCEALED_PAIR) {
			const rank = (set[1] >> 2)
			if (suit !== 3 && rank !== 1 && rank !== 9) {
				allOutside = false
			}
		} else {
			if (type === CHOICE_ADDED_KONG || type === CHOICE_CONCEALED_PUNG || type === CHOICE_CONCEALED_KONG || type === CHOICE_PUNG || type === CHOICE_KONG) {
				const rank = ((set[1] - 40) >> 2)
				if (suit !== 3 && rank !== 1 && rank !== 9) {
					allOutside = false
				}
			} else {
				allPungs = false
				const rank = ((set[1] - 80) >> 2)
				if (rank !== 1 && rank !== 7) {
					allOutside = false
				}
			}
		}
	}
	if (suits === 8) {
		fan.push([FAN_ALL_HONORS, 4])
	} else {
		if (suits === 1 || suits === 2 || suits === 4) {
			fan.push([FAN_FULL_FLUSH, 2])
		} else if (suits === 9 || suits === 10 || suits === 12) {
			fan.push([FAN_HALF_FLUSH, 1])
		}
		if (allPungs) {
			fan.push([FAN_ALL_PUNGS, 1])
		}
		if (allOutside) {
			fan.push([FAN_ALL_OUTSIDE, 1])
		}
		if (!allPungs) {
			if (!allOutside) for (let suit = 0; suit < 3; ++suit)if (searchSetFromArray(comb, [
				s => s[1] === (1 << 2 | suit) + 80,
				s => s[1] === (4 << 2 | suit) + 80,
				s => s[1] === (7 << 2 | suit) + 80,
			]).length) {
				fan.push([FAN_STRAIGHT, 1])
				break
			}
			for (let rank = 1; rank <= 7; ++rank)if (searchSetFromArray(comb, [
				s => s[1] === (rank << 2 | 0) + 80,
				s => s[1] === (rank << 2 | 1) + 80,
				s => s[1] === (rank << 2 | 2) + 80,
			]).length) {
				fan.push([FAN_MIXED_TRIPLE, 1])
				break
			}
		}
	}

	if (fan.length) {
		let points = 0
		for (const f of fan) {
			points += f[1]
		}
		return { points: points, fan: fan }
	} else {
		return undefined
	}
}
function hu(hand, tsumo) {
	let points = 0, hu = undefined
	for (const comb of _combs(hand)) {
		let c = calc(comb)
		if (c && c.points > points) {
			points = c.points
			hu = c
		}
	}
	if (points) {
		hu.tsumo = tsumo
	}
	return hu
}
function tenpai(hand) {
	let points = 0, tenpai = undefined, counter = newTileCounter()
	countTileArray(counter, hand.tiles)
	countTileMelds(counter, hand.melds)
	let newHand = structuredClone(hand)
	for (let suit = 0; suit < 4; ++suit) {
		for (let tile = (suit === 3 ? 31 : (36 | suit)); tile > 3; tile -= 4)if (counter[tile]) {
			newHand.tiles.push(tile)
			let h = hu(newHand)
			if (h && h.points > points) {
				tenpai = h
			}
			newHand.tiles.pop()
		}
	}
	return tenpai
}
function checkAddedMelds(tiles, currentMelds) {
	let melds = []

	// Check added kongs
	for (const meld of currentMelds) if (meld[0] === CHOICE_PUNG) {
		const value = meld[1] - 40
		for (const set of searchSetFromArray(tiles, [
			tile => getTileValue(tile) === value,
		])) {
			melds.push([CHOICE_ADDED_KONG, value + 40, 0, set])
		}
	}
	return melds
}
function checkConcealedMelds(tiles) {
	let melds = []

	// Check kongs
	for (const value of sortAndUnique(tiles.map(getTileValue))) {
		for (const set of searchSetFromArray(tiles, [
			tile => getTileValue(tile) === value,
			tile => getTileValue(tile) === value,
			tile => getTileValue(tile) === value,
			tile => getTileValue(tile) === value,
		])) {
			melds.push([CHOICE_CONCEALED_KONG, value + 40, 0, set])
		}
	}
	return melds
}
function checkMelds(tiles, discarded) {
	const d = getTileValue(discarded)
	let melds = []

	// Check chows
	if ((d & 3) !== 3) {
		// 12[3]
		if (d >= 12) {
			for (const set of searchSetFromArray(tiles, [
				tile => getTileValue(tile) === d - 8,
				tile => getTileValue(tile) === d - 4,
			])) {
				melds.push([CHOICE_CHOW, d + 72, discarded, set])
			}
		}
		// 1[2]3
		if (d >= 8 && d < 36) {
			for (const set of searchSetFromArray(tiles, [
				tile => getTileValue(tile) === d - 4,
				tile => getTileValue(tile) === d + 4,
			])) {
				melds.push([CHOICE_CHOW, d + 76, discarded, set])
			}
		}
		// [1]23
		if (d < 32) {
			for (const set of searchSetFromArray(tiles, [
				tile => getTileValue(tile) === d + 4,
				tile => getTileValue(tile) === d + 8,
			])) {
				melds.push([CHOICE_CHOW, d + 80, discarded, set])
			}
		}
	}

	// Check pungs
	for (const set of searchSetFromArray(tiles, [
		tile => getTileValue(tile) === d,
		tile => getTileValue(tile) === d,
	])) {
		melds.push([CHOICE_PUNG, d + 40, discarded, set])
	}

	// Check kongs
	for (const set of searchSetFromArray(tiles, [
		tile => getTileValue(tile) === d,
		tile => getTileValue(tile) === d,
		tile => getTileValue(tile) === d,
	])) {
		melds.push([CHOICE_KONG, d + 40, discarded, set])
	}
	return melds
}